﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Net;
using System.Net.Sockets;

namespace Common
{
    public static class IPAddressExtentions
    {
        private static List<int> validIPv4NetmaskValues = new List<int> { 0, 128, 192, 224, 240, 248, 252, 254, 255 };

        /// <summary>
        /// Gets the network address of the current network
        /// </summary>
        /// <param name="ipAddress"></param>
        /// <param name="netmask"></param>
        /// <returns>
        /// IPAddress of the netowrk address for the current network
        /// </returns>
        public static IPAddress GetIPv4NetworkAddress(this IPAddress ipAddress, IPAddress netmask)
        {
            if (ipAddress.AddressFamily != AddressFamily.InterNetwork || netmask.AddressFamily != AddressFamily.InterNetwork)
                return null;

            if (!netmask.IsIPv4NetMask())
                return null;

            var addressBytes = ipAddress.GetAddressBytes();
            var netmaskBytes = netmask.GetAddressBytes();

            if (addressBytes.Length != netmaskBytes.Length)
                return null;

            var networkBytes = new byte[addressBytes.Length];

            for (var i = 0; i < addressBytes.Length; i++)
            {
                networkBytes[i] = (byte)(addressBytes[i] & netmaskBytes[i]);
            }

            return new IPAddress(networkBytes);
        }

        /// <summary>
        /// Gets the broadcast address of the current network
        /// </summary>
        /// <param name="ipAddress"></param>
        /// <param name="netmask"></param>
        /// <returns>
        /// IPAddress of the broadcast address for the current network
        /// </returns>
        public static IPAddress GetIPv4BroadcastAddress(this IPAddress ipAddress, IPAddress netmask)
        {
            if (ipAddress.AddressFamily != AddressFamily.InterNetwork || netmask.AddressFamily != AddressFamily.InterNetwork)
                return null;

            if (!netmask.IsIPv4NetMask())
                return null;

            var addressBytes = ipAddress.GetAddressBytes();
            var netmaskBytes = netmask.GetAddressBytes();

            if (addressBytes.Length != netmaskBytes.Length)
                return null;

            var broadcastBytes = new byte[addressBytes.Length];

            for (var i = 0; i < addressBytes.Length; i++)
            {
                broadcastBytes[i] = (byte)(addressBytes[i] | (netmaskBytes[i] ^ 255));
            }

            return new IPAddress(broadcastBytes);
        }

        /// <summary>
        /// Gets all usable addresses in the specified network
        /// </summary>
        /// <param name="ipAddress"></param>
        /// <param name="netmask"></param>
        /// <returns>
        /// A list of the usable IPAddresses in the specified address.
        /// null if either address is not an IPv4 Address or if the netmask is not valid.
        /// </returns>
        public static List<IPAddress> GetAddressesInNetwork(this IPAddress ipAddress, IPAddress netmask)
        {
            if (ipAddress.AddressFamily != AddressFamily.InterNetwork || netmask.AddressFamily != AddressFamily.InterNetwork)
                return null;

            if (!netmask.IsIPv4NetMask())
                return null;

            var addressBytes = ipAddress.GetAddressBytes();
            var netmaskBytes = netmask.GetAddressBytes();

            if (addressBytes.Length != netmaskBytes.Length)
                return null;

            var ipAddresses = new List<IPAddress>();

            var networkAddress = ipAddress.GetIPv4NetworkAddress(netmask);
            var broadcastAddress = ipAddress.GetIPv4BroadcastAddress(netmask);

            var currentAddress = networkAddress.GetNextIPv4Address();

            while (currentAddress.CompareIPv4Addresses(broadcastAddress) < 0)
            {
                ipAddresses.Add(currentAddress);
                currentAddress = currentAddress.GetNextIPv4Address();
            }

            return ipAddresses;
        }

        /// <summary>
        /// Compares two addresses to determine if one is greater or less than the other
        /// </summary>
        /// <param name="ipAddress"></param>
        /// <returns>
        /// 1 if the current address is higher than the compare address, 0 if the addresses are equal and -1 if the current address is greater than the compare address. 
        /// Will throw an ApplicationException if either address is not and IPv4 Address.
        /// </returns>
        public static int CompareIPv4Addresses(this IPAddress ipAddress, IPAddress compareAddress)
        {
            if (ipAddress.AddressFamily != AddressFamily.InterNetwork || compareAddress.AddressFamily != AddressFamily.InterNetwork)
                throw new ApplicationException("Both addresses must be IPv4 Addresses");

            var ipAddressBytes = ipAddress.GetAddressBytes();
            var compareAddressBytes = compareAddress.GetAddressBytes();

            if (ipAddressBytes.Length != compareAddressBytes.Length)
                throw new ApplicationException("The length of both addresses do not match");

            for (var i = 0; i < ipAddressBytes.Length; i++)
            {
                if (ipAddressBytes[i] > compareAddressBytes[i])
                    return 1;
                if (ipAddressBytes[i] < compareAddressBytes[i])
                    return -1;
            }

            return 0;
        }

        /// <summary>
        /// Gets the next IPAddress.
        /// </summary>
        /// <param name="ipAddress"></param>
        /// <returns>
        /// <see cref="IPAddress"/>
        /// </returns>
        public static IPAddress GetNextIPv4Address(this IPAddress ipAddress)
        {
            if (ipAddress.AddressFamily != AddressFamily.InterNetwork)
                return null;

            var addressBytes = ipAddress.GetAddressBytes();

            if (addressBytes[3] < 255)
            {
                addressBytes[3]++;
            }
            else
            {
                addressBytes[3] = 0;
                if (addressBytes[2] < 255)
                {
                    addressBytes[2]++;
                }
                else
                {
                    addressBytes[2] = 0;
                    if (addressBytes[1] < 255)
                    {
                        addressBytes[1]++;
                    }
                    else
                    {

                        addressBytes[1] = 0;
                        if (addressBytes[0] < 255)
                        {
                            addressBytes[0]++;
                        }
                        else
                        {
                            return null;
                        }
                    }
                }
            }

            return new IPAddress(addressBytes);
        }

        /// <summary>
        /// Determines if the IPAddress is a networkmask.
        /// </summary>
        /// <param name="ipAddress"></param>
        /// <returns></returns>
        public static bool IsIPv4NetMask(this IPAddress ipAddress)
        {
            if (ipAddress.AddressFamily != AddressFamily.InterNetwork)
                return false;

            var addressBytes = ipAddress.GetAddressBytes();
            var interestingOctetHit = false;

            for (int i = 0; i < addressBytes.Length; i++)
            {
                if (!validIPv4NetmaskValues.Contains(addressBytes[i]))
                    return false;

                if (!interestingOctetHit && addressBytes[i] != 255)
                    interestingOctetHit = true;

                else if (interestingOctetHit && addressBytes[i] != 0)
                    return false;
            }

            return true;
        }
    }
}
